/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

/* PUBLIC
 * Created on 2/02/2005 by Grant Gardner
 * This particular class is public domain, regardless of other notices. Nobody should have to write this kind of stuff!!
 PUBLIC */

package au.com.lastweekend.jim.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

import org.apache.log4j.Logger;

/**
 * Quite why this can't be on File itself I don't know, well actually... directory operations, create etc are quite different from
 * copy.. but even so.. System.copyFile() ,something somewhere surely...
 * 
 * Apparently not, and google didn't find any for me either... I can't believe I'm writing this....
 * 
 * @author grant@lastweekend.com.au
 * @version $Id: FileOps.java,v 1.8 2006/04/16 09:19:47 ggardner Exp $
 */
public final class FileOps {
    public static final Logger LOG = Logger.getLogger(FileOps.class);

    public FileOps() {

        _trashDir = new File(System.getProperty("java.io.tmpdir"));
        LOG.info("Trash is " + _trashDir);
    }
    public static final FileOps INSTANCE = new FileOps();
    /**
     * Logger. Public so exceptions thrown by FileOps can be logged by the calling object.
     */
    private File _trashDir;

    /**
     * Copies files and directories. <br>
     * 
     * @param from
     * @param to
     * @throws IOException
     */
    
    public void copy(File from, File to) throws IOException {

        if (to.isDirectory()) {
            doCopy(from, new File(to, from.getName()));
        } else {
            doCopy(from, to);
        }

    }

    private void doCopy(File from, File to) throws IOException {

        if (from.isDirectory()) {
            copy(from.listFiles(), to);
        } else {
            FileChannel sourceChannel = new FileInputStream(from).getChannel();
            FileChannel destinationChannel = new FileOutputStream(to).getChannel();
            sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
            sourceChannel.close();
            destinationChannel.close();
        }
    }

    /**
     * Moves files and directories. <br>
     * path/name -> dir ... creates dir/name <br>
     * path/name -> ? ... creates ?
     * 
     * @param from
     * @param to
     * @throws IOException
     */
    public void move(File from, File to) throws IOException {

        if (to.isDirectory()) {
            doMove(from, new File(to, from.getName()));
        } else {
            doMove(from, to);
        }

    }

    /**
     * @param from
     * @param to
     * @throws IOException
     */
    private void doMove(File from, File to) throws IOException {

        from = from.getCanonicalFile();
        to = to.getCanonicalFile();

        if (from.equals(to)) {
            throw new IOException("Can't move a file to itself " + from + " -> " + to);
        }

        if (from.exists() && to.exists() && (!to.isDirectory())) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Deleting " + to + " to allow renameTo");
            }
            to.delete();
        }

        if (!from.renameTo(to)) {
            LOG.warn("Unable to use rename to move " + from.getPath() + " to " + to.getPath());

            if (to.getPath().startsWith(from.getPath() + File.separator)) {
                throw new IOException("Can't move a directory to a subdirectory of itself " + from + "->" + to);
            }

            doCopy(from, to);
            if (from.isDirectory()) {
                deleteDirectory(from);
            } else {
                if (!from.delete()) {
                    throw new IOException("Unable to delete " + from);
                }
            }
        }

    }

    /**
     * Move a array of files (or directories) to a directory
     * 
     * @param fileList
     * @param toDir
     * 
     * @throws IOException
     */
    public void move(File[] fileList, File toDir) throws IOException {

        move(Arrays.asList(fileList), toDir);
    }

    /**
     * Move a collection of files (or directories) to a directory
     * 
     * @param fileList
     * @param toDir
     * @throws IOException
     */
    public void move(Collection<? extends File> fileList, File toDir) throws IOException {

        makeAndCheckDirectory(toDir);

        for (File fileFrom : fileList) {

            File fileTo = new File(toDir, fileFrom.getName());
            move(fileFrom, fileTo);
        }
    }

    /**
     * Copies an array of files to a directory. The directory will be created if it doesn't exist
     * 
     * @param fileArray
     * @param toDir
     * @throws IOException
     */
    public void copy(File[] fileArray, File toDir) throws IOException {

        copy(Arrays.asList(fileArray), toDir);

    }

    /**
     * Copies a list of files to a directory. The directory will be created if it doesn't exist
     * 
     * @param files
     * @param toDir
     * @throws IOException
     * 
     */
    public void copy(Collection<? extends File> files, File toDir) throws IOException {

        makeAndCheckDirectory(toDir);

        for (File fileFrom : files) {

            File fileTo = new File(toDir, fileFrom.getName());
            doCopy(fileFrom, fileTo);
        }
    }

    private void makeAndCheckDirectory(File toDir) throws IOException {

        if (!toDir.exists()) {
            toDir.mkdir();
        }

        if (!toDir.isDirectory()) {
            throw new IOException(toDir + " is not a directory");
        }
    }

    /**
     * Recursively removes ALL files and subdirectories from this directory.
     * 
     * @param dir
     *            the directory to be deleted
     * @throws IOException
     */
    public void deleteDirectory(File dir) throws IOException {

        if (!dir.isDirectory()) {
            throw new IOException(dir + "is not a directory");
        }

        File[] files = dir.listFiles();
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                deleteDirectory(files[i]);
            } else {
                files[i].delete();
            }
        }

        dir.delete();
    }

    /**
     * Find files according to the filter, with a twist <br>
     * Directories matched by the filter will be searched recursively for files that match <br>
     * For full recursive searches the filter should match for all directories
     * 
     * 
     * @param huntDirectory
     * @param filter
     *            Directories returned by the filter will be searched recursively
     * @return Files from any directory below hunt directory that match the filter
     * 
     */
    public Collection<File> find(File huntDirectory, FileFilter filter) {

        Collection<File> c = new HashSet<File>();
        doFind(c, huntDirectory, filter);

        return c;

    }

    private void doFind(Collection<? super File> c, File huntDirectory, FileFilter filter) {

        File[] files = huntDirectory.listFiles(filter);
        for (int i = 0; i < files.length; i++) {
            File theFile = files[i];
            if (theFile.isDirectory()) {
                doFind(c, theFile, filter);
            } else {
                c.add(theFile);
            }
        }

    }

    /**
     * Java has no concept of, or access to, the "Trash" or "Recycle Bin"
     * 
     * TRASH is simply a directory where copies of the files are stored. If a file exists some random number will be added to the
     * extension.
     * 
     * 
     * 
     * @param file
     * @throws IOException
     */
    public void moveToTrash(File file) throws IOException {

        File trashFile = new File(_trashDir, file.getName());
        while (trashFile.exists()) {
            trashFile = new File(_trashDir, file.getName() + "." + Math.random() * 1000);
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("Deleting to trash by Moving " + file + " to " + trashFile);
        }
        move(file, trashFile);

    }

    /**
     * Test if two files are the same underlying file. Tests equality of canonicalized files. If you care about the exception
     * compare them yourself!
     * 
     * @param file1
     * @param file2
     * @return true if files refer to the same canonical file
     */
    public boolean areSameFile(File file1, File file2) {

        try {
            file1 = file1.getCanonicalFile();
            file2 = file2.getCanonicalFile();
        } catch (IOException e) {
            LOG.warn("Unable to canonicalize files for comparison ", e);
        }
        return file1.equals(file2);
    }

    /**
     * 
     * @param parentDir
     * @param possChild
     * @return
     * @throws IOException
     */
    public boolean subDirectoryOfSelf(File parentDir, File possChild) throws IOException {

        if (!parentDir.isDirectory()) {
            throw new IOException(parentDir.getAbsolutePath() + " is not a directory");
        }

        String fromPath = parentDir.getCanonicalPath() + File.pathSeparator;
        String toPath = possChild.getCanonicalPath();

        return toPath.startsWith(fromPath);

    }

    public void touch(File file) throws IOException {

        touch(file, System.currentTimeMillis());
    }

    public void touch(File file, long touchTime) throws IOException {

        if (!file.exists() && !file.createNewFile()) {
            throw new IOException("Unable to create " + file.getAbsolutePath());
        }

        if (!file.isFile()) {
            throw new IOException("Not a plain file " + file.getAbsolutePath());
        }

        if (!file.setLastModified(touchTime)) {
            throw new IOException("Unable to set last modified time for " + file.getAbsolutePath());
        }

    }

}